﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MovieRental.DataInterfaces;
using MovieRental.Entities;
using MovieRental.BuisnessComponents.Exceptions;
using MovieRental.BuisnessComponents.Helpers;
using MovieRental.BuisnessComponents.Base;

namespace MovieRental.BuisnessComponents
{
    public class LoanCartComponent : BaseComponent<LoanCartInfo, Guid, ILoanCartRepository>
    {
        public LoanCartComponent(ILoanCartRepository repository)
            : base(repository)
        {
        }

        public override void Verify(LoanCartInfo obj)
        {
            if (obj.LoanDate.Date > DateTime.Today.Date)
                throw new FieldValueRangeException<DateTime>(obj, "LoanDate", DateTime.Today, DateTime.MaxValue);
            if (obj.CustomerId == Guid.Empty)
                throw new EmptyFieldException(obj, "CustomerId");
            if (obj.Loans == null || !obj.Loans.Any())
                throw new EmptyFieldException(obj, "Loans");
            foreach (var loan in obj.Loans)
            {
                if (loan.EditionId == Guid.Empty)
                    throw new EmptyFieldException(obj, "Loans[].EditionId");
                if (loan.LoanCartId != obj.Id)
                    throw new ReferenceException("Loan cart exception");
                if (loan.PricePerDay < 0)
                    throw new FieldValueRangeException<double>(obj, "Loans[].PricePerDay", 0, double.MaxValue);
                if (loan.ReturnDate != DateTime.MinValue && loan.ReturnDate < DateTime.Today)
                    throw new FieldValueRangeException<DateTime>(obj, "Loans[].ReturnDay");
            }
        }

        public LoanCartInfo AssignCustomerToCart(LoanCartInfo cart, CustomerInfo customer)
        {
            cart.CustomerId = customer.Id;
            CalculateCartDiscounts(cart);
            OnSourceUpdated();
            return cart;
        }

        public void Delete(Guid cartId)
        {
            Repository.Delete(cartId);
        }

        private void AddLoanToCart(LoanCartInfo cart, EditionInfo edition)
        {
            LoanInfo loan = new LoanInfo();
            loan.LoanCartId = cart.Id;
            loan.EditionId = edition.Id;
            loan.ReturnDate = DateTime.MinValue;
            var tempDiscountComponent = ComponentFactory.Instance.GetTemporaryDiscountComponent();
            loan.PricePerDay = edition.PricePerDay * tempDiscountComponent.GetAvailableDiscountValue(edition).ToDiscountCoefficient();
            cart.Loans.Add(loan);
        }

        private void CalculateCartDiscounts(LoanCartInfo cart)
        {
            var cartDiscountComponent = ComponentFactory.Instance.GetCartDiscountComponent();
            var personalDiscountComponent = ComponentFactory.Instance.GetPersonalDiscountComponent();
            cart.CartDiscountValue = cartDiscountComponent.GetAvailableCartDiscountValue(cart.Loans.Count()) +
                personalDiscountComponent.GetAvailablePersonalDiscountValue(cart.CustomerId);
        }


        public LoanCartInfo AddEditionToCart(LoanCartInfo cart, EditionInfo edition)
        {
            if (cart == null || edition == null)
                throw new ArgumentNullException("Exception in loan cart component");
            if (cart.Loans.Any(x => x.EditionId == edition.Id))
                throw new ItemDuplicateException<Guid>(edition.Id);
            var editionComponent = ComponentFactory.Instance.GetEditionComponent();
            if (editionComponent.RemainingQuantity(edition) == 0)
                throw new NotEnoughException("No more editions to add!");
            AddLoanToCart(cart, edition);
            CalculateCartDiscounts(cart);
            return cart;
        }

        public LoanCartInfo RemoveEditionFromCart(LoanCartInfo cart, Guid editionId)
        {
            if (cart == null || editionId == Guid.Empty)
                throw new ArgumentNullException("Exception in loan cart component");
            if (!cart.Loans.Any(x => x.EditionId == editionId))
                throw new ItemNotFoundException<Guid>(editionId);
            LoanInfo loan = cart.Loans.Single(x => x.EditionId == editionId);
            cart.Loans.Remove(loan);
            CalculateCartDiscounts(cart);
            return cart;
        }

        //unused
        public double ReturnLoanAndGetFee(LoanCartInfo cart, EditionInfo edition)
        {
            if (cart == null || edition == null)
                throw new ArgumentNullException("Exception in loan cart component");
            var loan = cart.Loans.SingleOrDefault(x => x.EditionId == edition.Id);
            if (loan == null)
                return 0;
            loan.ReturnDate = DateTime.Now;
            Repository.Save(cart);
            return CalculateFee(loan);
        }

        public void ReturnLoans(IEnumerable<LoanInfo> loans)
        {
            if (loans == null || !loans.Any())
                throw new ArgumentNullException("Exception in loan cart component");
            var cart = Repository.Load(loans.First().LoanCartId);
            foreach (LoanInfo loan in cart.Loans.Where(l => loans.Any(x => l.EditionId == x.EditionId)))
                loan.ReturnDate = DateTime.Now;
            Repository.Save(cart);
            OnSourceUpdated();
        }

        public IEnumerable<LoanInfo> GetAllReturnedLoans(Guid customerId)
        {
            List<LoanInfo> returned = new List<LoanInfo>();
            foreach (var cart in Repository.LoadAll().Where(x => x.CustomerId == customerId))
            {
                returned.AddRange(cart.Loans.Where(x => x.ReturnDate != DateTime.MinValue));
            }
            return returned;
        }

        public double GetCartDiscountValue(Guid loanCartId)
        {
            return Repository.Load(loanCartId).CartDiscountValue;
        }

        private double CalculateFee(LoanInfo loan)
        {
            LoanCartInfo cart = Repository.Load(loan.LoanCartId);
            var days = (loan.ReturnDate - cart.LoanDate).Days + 1;
            return days * loan.PricePerDay * GetCartDiscountValue(loan.LoanCartId).ToDiscountCoefficient();
        }

        private double CalculateMustPayFee(LoanInfo loan)
        {
            LoanCartInfo cart = Repository.Load(loan.LoanCartId);
            var days = (DateTime.Now - cart.LoanDate).Days + 1;
            return days * loan.PricePerDay * GetCartDiscountValue(loan.LoanCartId).ToDiscountCoefficient();
        }

        public double CalculateMustPayFee(IEnumerable<LoanInfo> loans)
        {
            return loans.Sum(x => CalculateMustPayFee(x));
        }

        public IEnumerable<LoanInfo> GetNotReturnedLoansForEdition(Guid editionId)
        {
            List<LoanInfo> loans = new List<LoanInfo>();
            foreach (var cart in Repository.LoadAll())
            {
                loans.AddRange(cart.Loans.Where(x => (x.ReturnDate == DateTime.MinValue && x.EditionId == editionId)));
            }
            return loans;
        }

        public int GetNotReturnedLoansForMovieCount(MovieInfo movie)
        {
            var editionComponent = ComponentFactory.Instance.GetEditionComponent();
            return editionComponent.GetAllEditionsForMovie(movie.Id).Sum(x => GetNotReturnedLoansForEdition(x.Id).Count());
        }

        public double GetInitialCostPerDay(LoanCartInfo cart)
        {
            return Math.Round(cart.Loans.Sum(x => x.PricePerDay), 2);
        }

        public double GetTotalCostPerDay(LoanCartInfo cart)
        {
            return Math.Round(GetInitialCostPerDay(cart) * cart.CartDiscountValue.ToDiscountCoefficient(), 2);
        }

        public IEnumerable<LoanInfo> GetAllNotReturnedLoans(Guid customerId)
        {
            List<LoanInfo> loans = new List<LoanInfo>();
            foreach (var cart in Repository.LoadAll().Where(x => x.CustomerId == customerId))
            {
                loans.AddRange(cart.Loans.Where(x => x.ReturnDate == DateTime.MinValue));
            }
            return loans;
        }

        public IEnumerable<LoanCartInfo> GetAllNotReturnedForDay(DateTime date)
        {
            return Repository.LoadAllForDay(date).Where(x => !AllLoansReturned(x));
        }

        public IEnumerable<LoanCartInfo> GetAllNotReturnedForDay(DateTime date, string searchString)
        {
            if (string.IsNullOrEmpty(searchString))
                return GetAllNotReturnedForDay(date);
            var customerComponent = ComponentFactory.Instance.GetCustomerComponent();
            Func<CustomerInfo, string> fullName = x => x.FirstName + " " + x.LastName;
            var query = from cart in GetAllNotReturnedForDay(date)
                        where fullName(customerComponent.Get(cart.CustomerId)).GetSimilarWordsCoefficient(searchString) > 0
                        orderby fullName(customerComponent.Get(cart.CustomerId)).GetSimilarWordsCoefficient(searchString) descending
                        select cart;
            return query;
        }

        public IEnumerable<LoanCartInfo> GetAllNotReturnedForCustomer(CustomerInfo customer)
        {
            return GetAll().Where(x => x.CustomerId == customer.Id && !AllLoansReturned(x));
        }

        public int GetReturnedEditionsCountForCustomer(CustomerInfo customer)
        {
            var query = from cart in Repository.LoadAllForCustomer(customer.Id)
                        select cart.Loans.Where(x => x.ReturnDate == DateTime.MinValue);
            return query.Count();
        }

        public double GetIncome(DateTime date)
        {
            var query = from cart in GetAll()
                        select cart.Loans.Where(x => x.ReturnDate.Date == date.Date);
            return query.Sum(set => set.Sum(loan => CalculateFee(loan)));         
        }

        public IEnumerable<KeyValuePair<DateTime, double>> GetIncomeForPeriod(DateTime startDate, DateTime endDate)
        {
            DateTime d = startDate.Date;
            while (d <= endDate.Date)
            {
                yield return new KeyValuePair<DateTime, double>(d, GetIncome(d));
                d = d.AddDays(1);
            }
        }

        public int GetLoanCount(DateTime date)
        {
            var query = from cart in GetAll()
                        where cart.LoanDate.Date == date.Date
                        select cart;
            return query.Sum(x => x.Loans.Count());
        }

        private bool AllLoansReturned(LoanCartInfo cart)
        {
            return cart.Loans.All(x => x.ReturnDate != DateTime.MinValue);
        }

        public double CountSpentMoney(Guid customerId)
        {
            return GetAllReturnedLoans(customerId).Sum(x => CalculateFee(x));
        }

        public int GetAllLoansCountForMovie(MovieInfo movie)
        {
            var editionComponent = ComponentFactory.Instance.GetEditionComponent();
            var editionIds = editionComponent.GetAllEditionsForMovie(movie.Id).Select(x => x.Id);
            var query = GetAll().Select(c => c.Loans.Where(l => editionIds.Contains(l.EditionId)));
            return query.Sum(x => x.Count());
        }

        public double CountMustReturnMoney(Guid customerId)
        {
            return GetAllNotReturnedLoans(customerId).Sum(x => CalculateMustPayFee(x));
        }

        public int GetNotReturnedEditionsCount(Guid customerId)
        {
            int count = 0;
            foreach (var cart in Repository.LoadAll().Where(x => x.CustomerId == customerId))
                count += cart.Loans.Count(x => x.ReturnDate == DateTime.MinValue);
            return count;      
        }

        
    }
}
